﻿/****************************************************************************************
*	Mouzetrap - client/server framework	
*
*	$HeadURL: http://mouzetrap.googlecode.com/svn/trunk/Mouzetrap/Configuration/ConfigurationManager.cs $
*	$Id: ConfigurationManager.cs 11 2009-06-11 09:10:25Z tobz@fallenrealms.net $
* 
*	Copyright (c) 2008 Toby Lawrence
* 
*	Permission is hereby granted, free of charge, to any person obtaining a copy
*	of this software and associated documentation files (the "Software"), to deal
*	in the Software without restriction, including without limitation the rights
*	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
*	copies of the Software, and to permit persons to whom the Software is
*	furnished to do so, subject to the following conditions:
* 
*	The above copyright notice and this permission notice shall be included in
*	all copies or substantial portions of the Software.
* 
*	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
*	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
*	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
*	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
*	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
*	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
*	THE SOFTWARE.
* 
****************************************************************************************/

using System;
using System.ComponentModel.Composition;
using System.IO;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.XPath;
using Mouzetrap.Localization;

namespace Mouzetrap.Configuration
{
	/// <summary>
	/// Provides generalized access to the application configuration based on XPath expressions.
	/// </summary>
	/// <remarks>
	/// Mouzetrap has its own required schema for networking, and thus will validate that 
	/// portion only.  The whole configuration file is validated  for well-formedness, however.
	/// </remarks>
	[Export]
	[Export(typeof(IConfiguration))]
	[CompositionOptions(CreationPolicy = CreationPolicy.Shared)]
	public class ConfigurationManager : IConfiguration
	{
		private readonly FileSystemWatcher _configWatcher;
		private XDocument _configuration;
		private string _configurationFile;

		internal ConfigurationManager()
		{
			// we use the app config by default
			_configurationFile = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;

			// update to latest configuration
			ChangeConfiguration(_configurationFile);

			// set up our configuration file watcher
			_configWatcher = new FileSystemWatcher(Path.GetDirectoryName(_configurationFile));
			_configWatcher.Changed += OnConfigurationChanged;
			_configWatcher.EnableRaisingEvents = true;
		}

		#region IConfiguration Members

		/// <summary>
		/// Gets an <see cref="XElement" /> from the configuration based on an XPath expression.
		/// </summary>
		/// <param name="xpathExpression">the XPath expression to select the desired node</param>
		/// <returns>an <see cref="XElement" /> representing the retrieved element, or null</returns>
		public XElement GetElement(string xpathExpression)
		{
			XElement element = _configuration.XPathSelectElement(xpathExpression);

			return element;
		}

		/// <summary>
		/// Gets a value from the configuration based on an XPath expression.
		/// </summary>
		/// <typeparam name="T">the type of the value to return</typeparam>
		/// <param name="xpathExpression">the XPath expression to select the desired node</param>
		/// <param name="value">the value to be returned</param>
		/// <returns>true if the value was found and able to be casted to the specified type; false otherwise</returns>
		public bool GetValue<T>(string xpathExpression, out T value) where T : IConvertible
		{
			value = default(T);

			// grab our element by the xpath query
			XElement element = _configuration.XPathSelectElement(xpathExpression);

			// couldn't find the element, no good
			if(element == null)
			{
				return false;
			}

			// try to cast to the proper type. if we can't, no good.
			try
			{
				value = (T) Convert.ChangeType(element.Value, typeof(T));
			}
			catch(Exception)
			{
				return false;
			}

			return true;
		}

		/// <summary>
		/// Sets a value in the configuration based on an XPath expression.
		/// </summary>
		/// <typeparam name="T">the type of the value being set</typeparam>
		/// <param name="xpathExpression">the XPath expression to select the desired node</param>
		/// <param name="value">the value to set</param>
		/// <returns>true if the value was set successfully; false otherwise</returns>
		public bool SetValue<T>(string xpathExpression, T value) where T : IConvertible
		{
			// grab our element by the xpath query
			XElement element = _configuration.XPathSelectElement(xpathExpression);

			// throw an exception because we don't want t
			if(element == null)
			{
				return false;
			}

			try
			{
				element.SetValue(value);
			}
			catch(Exception)
			{
				return false;
			}

			_configuration.Save(_configurationFile);

			return true;
		}

		#endregion

		/// <summary>
		/// Event for external configuration changes.
		/// </summary>
		/// <remarks>In efforts to keep change notification functionality without massive complexity,
		/// this event will fire even when values are set internally. It's not a huge deal, but if you
		/// subscribe to the event, and change a value, you'll get a configuration updated notification.</remarks>
		public event EventHandler ConfigurationChanged;

		private void OnConfigurationChanged(object sender, FileSystemEventArgs e)
		{
			// if it's our config file, notify listeners
			if(e.FullPath == _configurationFile)
			{
				EventHandler handler = ConfigurationChanged;

				if(handler != null)
				{
					handler(null, null);
				}
			}
		}

		/// <summary>
		/// Changes the configuration file being used.
		/// </summary>
		/// <param name="configFile">the path to the new configuration file to use</param>
		public void ChangeConfiguration(string configFile)
		{
			// adjust relative paths to full paths in relation to
			// our current app domain's config file path.
			// this should keep relative paths accurate if we're
			// being run inside a custom app domain with a custom location
			// or anything.
			if(!Path.IsPathRooted(configFile))
			{
				// relative path, adjust it to our appdomain's config path
				string currentConfigPath = Path.GetDirectoryName(_configurationFile);

				configFile = Path.Combine(currentConfigPath, Path.GetFileName(configFile));
			}

			// check to make sure new config exists
			if(!File.Exists(configFile))
			{
				throw new FileNotFoundException(Mouzetrap_Messages.ConfigFileDoesntExist, configFile);
			}

			// opening the file will atleast check it for well-formedness. we don't try
			// and catch any exceptions here, because their propagation forces the user 
			// to make sure they aren't feeding us garbage.
			var validator = new XmlDocument();
			validator.Load(configFile);

			// get our configuration block. this lets us validate against our mouzetrap configuration
			// and the app config can contain other stuff and not cause validation to fail.
			XmlNode mouzetrapSection =
				validator.SelectSingleNode(ConfigurationConstants.CONFIGURATION_XML_XPATH_ROOT);

			if(mouzetrapSection == null)
			{
				throw new XmlException(Mouzetrap_Messages.InvalidConfigurationFile);
			}

			// get our schema and add it before validation. throw the exceptions from validation
			// so people can go and fix their configurations.
			using(Stream schemaStream = ResourceHelper.GetStream(ConfigurationConstants.CONFIGURATION_SCHEMA))
			{
				validator.Schemas.Add(null, XmlReader.Create(schemaStream));
				validator.Validate(
					(sender, args) =>
						{
							if(args.Severity == XmlSeverityType.Error)
							{
								throw args.Exception;
							}
						},
					mouzetrapSection);

				// we've passed well-formedness/schema validation at this point, configuration can be changed
				_configurationFile = configFile;
			}

			// load the configuration
			_configuration = XDocument.Load(_configurationFile);
		}
	}
}